﻿using System;
using System.Collections.Generic;
using System.Reflection;

namespace BizElements.Generator
{
    public interface IAssemblyMetaDataSource
    {
        IEnumerable<AssemblyItem> GetItems(Assembly asm, IAssemblyItemCriteria criteria);
    }

    public class AssemblyMetaDataSource : IAssemblyMetaDataSource
    {
        public IEnumerable<AssemblyItem> GetItems(Assembly asm, IAssemblyItemCriteria criteria)
        {
            Type[] allTypes = asm.GetTypes();
            List<AssemblyItem> matches = new List<AssemblyItem>();
            foreach (Type type in allTypes)
            {
                if (criteria.IsMatch(type))
                    matches.Add(new AssemblyItem(type));
            }

            matches.Sort((x, y) => x.Name.CompareTo(y.Name));
            return matches;
        }
    }

    public sealed class AssemblyItem
    {
        public AssemblyItem(Type type)
        {
            this.Name = type.FullName;
            this.Type = type;
        }

        public string Name { get; private set; }
        public Type Type { get; private set; }
    }

    public interface IAssemblyItemCriteria
    {
        bool IsMatch(Type assemblyItem);
    }

    public class AssemblyItemCriteria : IAssemblyItemCriteria
    {
        #region IsMatch.

        public bool IsMatch(Type type)
        {
            if (!IsSubClassCriteriaMatched(type))
                return false;

            bool shouldCheckRequiredInterfaces = (this.RequiredInterfaces != null) && (this.RequiredInterfaces.Length > 0);
            bool shouldCheckAlterInterfaces = (this.AlternativeInterfaces != null) && (this.AlternativeInterfaces.Length > 0);
            if (shouldCheckRequiredInterfaces || shouldCheckAlterInterfaces)
            {
                var allInterfaces = GetInterfaces(type);
                if (!ImplementsAllRequiredInterfaces(allInterfaces))
                    return false;

                if (!ImplementsOneOfAlternativeInterfaces(allInterfaces))
                    return false;
            }

            if (this.AdditionalCriteria != null)
                return AdditionalCriteria(type);

            return true;
        }

        private bool IsSubClassCriteriaMatched(Type type)
        {
            if (!string.IsNullOrEmpty(this.BaseClass))
            {
                var baseClasses = GetBaseClasses(type);
                bool isNotSubClass = !baseClasses.Contains(this.BaseClass);
                if (isNotSubClass)
                    return false;
            }

            return true;
        }

        private bool ImplementsAllRequiredInterfaces(List<string> interfacesImplementedByType)
        {
            if ((this.RequiredInterfaces != null) && (this.RequiredInterfaces.Length > 0))
            {
                foreach (string reqInterface in this.RequiredInterfaces)
                {
                    bool isNotImplemented = !interfacesImplementedByType.Contains(reqInterface);
                    if (isNotImplemented)
                        return false;
                }
            }

            return true;
        }

        private bool ImplementsOneOfAlternativeInterfaces(List<string> interfacesImplementedByType)
        {
            if ((this.AlternativeInterfaces == null) || (this.AlternativeInterfaces.Length == 0))
                return true;

            foreach (string altInterface in this.AlternativeInterfaces)
            {
                bool isImplemented = interfacesImplementedByType.Contains(altInterface);
                if (isImplemented)
                    return true;
            }

            return false;
        }

        private static List<string> GetBaseClasses(Type subClass)
        {
            List<string> baseClasses = new List<string>();
            Type currBaseClass = subClass.BaseType;
            while ((currBaseClass != null) && currBaseClass != typeof(object))
            {
                baseClasses.Add(currBaseClass.Name);
                currBaseClass = currBaseClass.BaseType;
            }

            return baseClasses;
        }

        private static List<string> GetInterfaces(Type type)
        {
            List<string> allInterfaces = new List<string>();
            foreach (Type inter in type.GetInterfaces())
                allInterfaces.Add(inter.Name);

            return allInterfaces;
        }

        #endregion

        #region Properties.

        public string BaseClass { get; set; }
        public string[] RequiredInterfaces { get; set; }
        public string[] AlternativeInterfaces { get; set; }
        public Predicate<Type> AdditionalCriteria { get; set; }

        #endregion
    }
}